#
# The main window of Ruby Roller.
#
# Auther: Wesley Liu
# Email: jinvy.liu@gmail.com
# Date: 12/18/2007
#

require 'fox16'
begin
  require 'opengl'
rescue LoadError
  require 'fox16/missingdep'
  MSG = <<EOM
  Sorry, Ruby Roller depends on the OpenGL extension. Please
  check the Ruby Application Archives for an appropriate
  download site.
EOM
  missingDependency(MSG)
end

require 'Parser/ParserManager'

include Fox

class RRWindow < FXMainWindow

  def initialize(app)
    # Invoke the base class initializer
    super(app, "Ruby Roller", :opts => DECOR_ALL, :width => 800, :height => 600)

    # Initialize ParserManager
    @parserManager = ParserManager.new
    
    # Main Icon
    setIcon(loadIcon('rubyroller', 'png'))
    
    # Menu
    menubar = FXMenuBar.new(self, LAYOUT_FILL_X)
    # Menu file
    filemenu = FXMenuPane.new(self)
    FXMenuTitle.new(menubar, "&File", nil, filemenu)
    # Menu file open
    FXMenuCommand.new(filemenu, "&Open\tCtl-O", nil).connect(SEL_COMMAND) do
        onOpenFile
    end
    FXMenuCommand.new(filemenu, "&Quit\tCtl-Q", nil, getApp(), FXApp::ID_QUIT)
    # Menu view
    viewmenu = FXMenuPane.new(self)
    FXMenuTitle.new(menubar, "&View", nil, viewmenu)    
    # Menu view fit
    FXMenuCommand.new(viewmenu, "&Fit\tCtl-T", nil).connect(SEL_COMMAND) do
        onFit
    end
    # Menu view rotate
    FXMenuCommand.new(viewmenu, "&Rotate\tCtl-R", nil).connect(SEL_COMMAND) do
        onRotate
    end
    # Menu view show bounding box
    FXMenuCommand.new(viewmenu, "&ShowBB\tCtl-B", nil).connect(SEL_COMMAND) do
        onBB
    end
    
    # Toolbar
    FXHorizontalSeparator.new(self, LAYOUT_SIDE_TOP|SEPARATOR_GROOVE|LAYOUT_FILL_X);
    toolbar = FXToolBar.new(self, LAYOUT_SIDE_TOP|LAYOUT_FILL_X,
      :padLeft => 4, :padRight => 4, :padTop => 0, :padBottom => 0, :hSpacing => 0, :vSpacing => 0)
    # Toolbar open
    openBtn = FXButton.new(toolbar, "\tOpen\tOpen 3D file.", loadIcon('openfile', 'gif'), nil, 0,
        FRAME_THICK|FRAME_RAISED|LAYOUT_TOP|LAYOUT_LEFT|BUTTON_TOOLBAR)
    openBtn.connect(SEL_COMMAND, method(:onOpenFile))
    # Toolbar fit
    fitBtn = FXButton.new(toolbar, "\tFit\tFit the model.", loadIcon('fit', 'gif'), nil, 0,
        FRAME_THICK|FRAME_RAISED|LAYOUT_TOP|LAYOUT_LEFT|BUTTON_TOOLBAR)
    fitBtn.connect(SEL_COMMAND, method(:onFit))
    # Toolbar rotate
    rotateBtn = FXButton.new(toolbar, "\tRotate\tRotate the model", loadIcon('rotate', 'gif'), nil, 0,
        FRAME_THICK|FRAME_RAISED|LAYOUT_TOP|LAYOUT_LEFT|BUTTON_TOOLBAR)
    rotateBtn.connect(SEL_COMMAND, method(:onRotate))
    # Toolbar show bounding box
    rotateBtn = FXButton.new(toolbar, "\tBB\tShow bounding box", loadIcon('showaabb', 'gif'), nil, 0,
        FRAME_THICK|FRAME_RAISED|LAYOUT_TOP|LAYOUT_LEFT|BUTTON_TOOLBAR)
    rotateBtn.connect(SEL_COMMAND, method(:onBB))
    
    # Make status bar
    statusbar = FXStatusBar.new(self, LAYOUT_SIDE_BOTTOM|LAYOUT_FILL_X|STATUSBAR_WITH_DRAGCORNER)    
    
    # Construct the main window elements
    frame = FXHorizontalFrame.new(self, LAYOUT_SIDE_TOP|LAYOUT_FILL_X|LAYOUT_FILL_Y)
    frame.padLeft, frame.padRight = 0, 0
    frame.padTop, frame.padBottom = 0, 0

    # Main pane to contain the glcanvas
    glcanvasFrame = FXVerticalFrame.new(frame,
      LAYOUT_FILL_X|LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT)
    glcanvasFrame.padLeft, glcanvasFrame.padRight = 10, 10
    glcanvasFrame.padTop, glcanvasFrame.padBottom = 10, 10
  
    # Drawing glcanvas
    glpanel = FXVerticalFrame.new(glcanvasFrame, (FRAME_SUNKEN|FRAME_THICK|
      LAYOUT_FILL_X|LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT))
    glpanel.padLeft, glpanel.padRight = 0, 0
    glpanel.padTop, glpanel.padBottom = 0, 0
      
    # A visual to draw OpenGL
    @glvisual = FXGLVisual.new(getApp(), VISUAL_DOUBLEBUFFER)
  
    # Drawing glcanvas
    @glcanvas = FXGLCanvas.new(glpanel, @glvisual, :opts => LAYOUT_FILL_X|LAYOUT_FILL_Y|LAYOUT_TOP|LAYOUT_LEFT)
    @glcanvas.connect(SEL_PAINT) { drawScene }
    @glcanvas.connect(SEL_CONFIGURE) do
      if @glcanvas.makeCurrent
        GL.Viewport(0, 0, @glcanvas.width, @glcanvas.height)
        @glcanvas.makeNonCurrent
      end
    end
    
    # Progress Dialog
    @progressdialog = FXProgressDialog.new(self, "Progress", "Incoming...", DECOR_BORDER|DECOR_RESIZE)
    @progressdialog.selector = FXDataTarget::ID_VALUE
    
    # Initialize rotating angle
    @angle = 0
    @rotating = false
    
    # Initialize show aabb flag
    @showbb = false

    # Idle process
    getApp().addChore(:repeat => true) do
        if @rotating == true
            @angle += 2.0
            if @angle > 360.0
              @angle -= 360.0
            end
        end
        beginTime = Time.now
        drawScene
        endTime = Time.now
        status = statusbar.statusLine.text
        if /(.+)\s+FPS.+/ =~ status
            status = $1
        end
        status = sprintf "%-50s FPS: %.2f", status, 1/(endTime - beginTime)
        statusbar.statusLine.text = status
    end
        
  end

  # Load the named PNG icon from a file
  def loadIcon(filename, suffix)
    begin
      filename = File.join("../res", filename) + '.' + suffix
      icon = nil
      File.open(filename, "rb") do |f|
        case suffix.downcase
        when 'png'
            icon = FXPNGIcon.new(getApp(), f.read)
        when 'gif'
            icon = FXGIFIcon.new(getApp(), f.read)
        end
      end
      icon.scale 16, 16
      icon
    rescue
      raise RuntimeError, "Couldn't load icon: #{filename}"
    end
  end
  
  def onOpenFile(sender=nil, sel=nil, ptr=nil)
    FXFileDialog.new(self, "Open File") do |fileDlg|
        patternStr = '*.' + @parserManager.parsers.keys.join(',*.')
        fileDlg.setPatternList patternStr
        if fileDlg.execute() != 0
            filename = fileDlg.filename
            fileDlg.close
            @progressdialog.message = "Importing " + File.basename(filename)
            progressTarget = FXDataTarget.new(0)
            @progressdialog.target = progressTarget
            @progressdialog.show(PLACEMENT_OWNER)
            Thread.new do
                @currentscene = @parserManager.parse filename, progressTarget
                @progressdialog.hide
            end
        end
    end  
  end
  
  def onFit(sender=nil, sel=nil, ptr=nil)
    if @currentscene != nil
        bigbb = @currentscene.rootbb
        center = bigbb.center
        minz = bigbb.aabb2v[0][2]
        maxz = bigbb.aabb2v[1][2]
        vlen = (bigbb.aabb2v[1] - bigbb.aabb2v[0]).r
        # adjust view
        @currentscene.look_position = [center[0], center[1], maxz + vlen/2]
        @currentscene.look_target = center
        # adjust frustum
        @currentscene.far = (maxz-minz)/2 + vlen
        # adjust light
        @currentscene.light_position = @currentscene.look_position
    end
  end
  
  def onRotate(sender=nil, sel=nil, ptr=nil)
    @rotating = !@rotating
  end
  
  def onBB(sender=nil, sel=nil, ptr=nil)
    @showbb = !@showbb
  end
  
  # Create and initialize
  def create
    super
    show(PLACEMENT_SCREEN)
  end
  
  # Draw the GL scene
  def drawScene 
    width = @glcanvas.width.to_f
    height = @glcanvas.height.to_f
    aspect = width/height
  
    # Make context current
    @glcanvas.makeCurrent()
    
    GL.Viewport(0, 0, @glcanvas.width, @glcanvas.height)
  
    GL.ClearColor(1.0, 1.0, 1.0, 1.0)
    GL.Clear(GL::COLOR_BUFFER_BIT|GL::DEPTH_BUFFER_BIT)
    GL.Enable(GL::DEPTH_TEST)
    GL.Disable(GL::DITHER)
  
      if @currentscene != nil
        GL.MatrixMode(GL::PROJECTION)
        GL.LoadIdentity()
        GLU.Perspective(90.0, aspect, @currentscene.near, @currentscene.far)
      
        GL.MatrixMode(GL::MODELVIEW)
        GL.LoadIdentity()
        GLU.LookAt(
            @currentscene.look_position[0],
            @currentscene.look_position[1],
            @currentscene.look_position[2],
            @currentscene.look_target[0],
            @currentscene.look_target[1],
            @currentscene.look_target[2],
            @currentscene.look_up[0],
            @currentscene.look_up[1],
            @currentscene.look_up[2])

        GL.ShadeModel(GL::SMOOTH)
        GL.Light(GL::LIGHT0, GL::POSITION, @currentscene.light_position)
        GL.Light(GL::LIGHT0, GL::AMBIENT, @currentscene.ambiant_light)
        GL.Light(GL::LIGHT0, GL::DIFFUSE, @currentscene.diffuse_light)
        GL.Enable(GL::LIGHT0)
        GL.Enable(GL::LIGHTING)
      
        GL.PushMatrix()
        center = @currentscene.rootbb.center
        GL.Rotatef(@angle, center[0], center[1], center[2])

        # Render the scene meshes
        GL.EnableClientState GL::VERTEX_ARRAY
        GL.EnableClientState GL::NORMAL_ARRAY
        @currentscene.each do |mesh|
            if mesh.material != nil
                GL.Material(GL::FRONT, GL::AMBIENT, mesh.material.ambient) if mesh.material.ambient != nil
                GL.Material(GL::FRONT, GL::DIFFUSE, mesh.material.diffuse) if mesh.material.diffuse != nil
                GL.Material(GL::FRONT, GL::SPECULAR, mesh.material.specular) if mesh.material.specular != nil
                GL.Material(GL::FRONT, GL::SHININESS, mesh.material.shininess) if mesh.material.shininess != nil
            else
                GL.Material(GL::FRONT, GL::AMBIENT, [0.5,0.5,0.5,1.0])
                GL.Material(GL::FRONT, GL::DIFFUSE, [0.5,0.5,0.5,1.0])
                GL.Material(GL::FRONT, GL::SPECULAR, [0.0,0.0,0.0,1.0])
                GL.Material(GL::FRONT, GL::SHININESS, 0.1)
            end
            GL.NormalPointer GL::FLOAT, 0, mesh.normalsPack
            GL.VertexPointer 3, GL::FLOAT, 0, mesh.verticesPack
            GL.DrawElements GL::TRIANGLES, mesh.faces.length * 3, GL::UNSIGNED_INT, mesh.facesPack
        end
        
        # Render the bounding boxes
        if @showbb == true
            @currentscene.each do |mesh|
                GL.VertexPointer 3, GL::FLOAT, 0, mesh.getAABB.cubeVerticesPack
                GL.DrawElements GL::LINES, 24, GL::UNSIGNED_INT, mesh.getAABB.cubeIndicesPack
            end
        end
        
        GL.PopMatrix()

    end
    
    # Swap if it is double-buffered
    if @glvisual.isDoubleBuffer
      @glcanvas.swapBuffers
    end
    
    # Make context non-current
    @glcanvas.makeNonCurrent
    
  end # drawScene end
  
end # class end


# Main

# Construct the application
application = FXApp.new("Ruby Roller", "Ruby Roller")

# Construct the main window
RRWindow.new(application)

# Create the app's windows
application.create

# Run the application
application.run

